// Copyright 2019 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"errors"
	"mi/compiler/ast"
	"mi/internal/span"
	"mi/internal/xpath"
	"regexp"
)

type NavigationTree struct {
	/** PkgName of the declaration, or a short description, e.g. "<class>". */
	Text string
	/** A ScriptElementKind */
	Kind ScriptElementKind
	/** ScriptElementKindModifier separated by commas, e.g. "public,abstract" */
	kindModifiers string
	/**
	 * Spans of the nodes that generated this declaration.
	 * There will be more than one if this is the result of merging.
	 */
	Span span.UTF16Span
	/** Present if non-empty */
	ChildItems []*NavigationTree
}

func (s *session) GetNavigationTree(ctx context.Context, uri xpath.URI) (*NavigationTree, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, errors.New("Unknown file " + uri.Filename())
	}

	return resolveDocumentSymbols(sourceFile), nil
}

type documentSymbols struct {
	container *NavigationTree
}

func resolveDocumentSymbols(node *ast.SourceFile) *NavigationTree {
	var resolve = new(documentSymbols)
	var root = resolve.getTreeOfNode(node)
	resolve.container = root
	resolve.resolveChildren(node)
	return root
}

func (ds *documentSymbols) pushTree(tree *NavigationTree) {
	if ds.container == nil || tree == nil {
		return
	}
	ds.container.ChildItems = append(ds.container.ChildItems, tree)
}

func (ds *documentSymbols) getTreeOfNode(node ast.Node) *NavigationTree {
	return &NavigationTree{
		Text:          ds.getDisplayName(node),
		Kind:          ds.getNodeKind(node),
		kindModifiers: ds.getModifiers(node),
		Span:          getNodeSpan(node),
		ChildItems:    nil,
	}
}

func (ds *documentSymbols) pushNode(node ast.Node) {
	ds.pushTree(ds.getTreeOfNode(node))
}

func (ds *documentSymbols) getDisplayName(node ast.Node) string {
	switch node.(type) {
	case *ast.SourceFile:
		return "<文件>"
	case *ast.ArrowFunctionDeclaration:
		return "<匿名函数>"
	case *ast.BodyDeclaration:
		return "<匿名类>"
	case *ast.ConstructorDeclaration:
		return "<构造>"
	}

	var name = ast.GetDeclarationName(node)
	return ast.DeclarationNameToString(name)
}

func (ds *documentSymbols) resolveEach(nodes ast.List) bool {
	if nodes != nil {
		for i := 0; i < nodes.Len(); i ++ {
			ds.resolve(nodes.NodeAt(i))
		}
	}

	return false
}

func (ds *documentSymbols) resolveEachChild(node ast.Node) {
	ast.ForEachChild(node, ds.resolve, ds.resolveEach)
}

func (ds *documentSymbols) isContainer(node ast.Node) bool {
	switch node.(type) {
	case *ast.SourceFile,
		*ast.ClassDeclaration,
		*ast.InterfaceDeclaration,
		*ast.EnumDeclaration,
		*ast.AnnotationDeclaration,
		*ast.MethodDeclaration,
		*ast.ConstructorDeclaration,
		*ast.BodyDeclaration:
		return true
	}
	return false
}

func (ds *documentSymbols) resolve(node ast.Node) bool {
	if ds.isContainer(node) {
		ds.resolveContainer(node)
	} else {
		ds.resolveChildren(node)
	}

	return false
}

func (ds *documentSymbols) resolveContainer(node ast.Node) {
	var container = ds.getTreeOfNode(node)
	ds.pushTree(container)
	var saveContainer = ds.container
	ds.container = container
	ds.resolveChildren(node)
	ds.container = saveContainer
}

func (ds *documentSymbols) resolveChildren(node ast.Node) bool {
	switch node.(type) {
	case *ast.ConstructorDeclaration:
		ds.pushNode(node)
	case *ast.FieldDeclaration:
		ds.pushNode(node)
	case *ast.MethodDeclaration:
		ds.pushNode(node)
	case *ast.ParameterDeclaration:
		ds.pushNode(node)
	case *ast.VariableDeclaration:
		ds.pushNode(node)
	case *ast.EnumItemDeclaration:
		ds.pushNode(node)
	default:
		ds.resolveEachChild(node)
	}

	return false
}

func (ds *documentSymbols) getNodeKind(node ast.Node) ScriptElementKind {
	switch node.(type) {
	case *ast.SourceFile:
		return SEKScriptElement
	case *ast.ClassDeclaration:
		return SEKClassElement
	case *ast.InterfaceDeclaration:
		return SEKInterfaceElement
	case *ast.EnumDeclaration:
		return SEKEnumElement
	case *ast.VariableDeclaration:
		return SEKVariableElement
	case *ast.ArrowFunctionDeclaration:
		return SEKFunctionElement
	case *ast.MethodDeclaration:
		return SEKMemberFunctionElement
	case *ast.FieldDeclaration:
		return SEKMemberVariableElement
	case *ast.ConstructorDeclaration:
		return SEKConstructSignatureElement
	case *ast.TypeParameterDeclaration:
		return SEKTypeParameterElement
	case *ast.EnumItemDeclaration:
		return SEKEnumMemberElement
	case *ast.ParameterDeclaration:
		return SEKParameterElement
	default:
		return SEKUnknown
	}
}

func (ds *documentSymbols) getModifiers(node ast.Node) string {
	return ""
	//if ast.Parent != nil && ast.Parent.Kind == syntaxkind.VariableDeclaration {
	//	ast = ast.Parent
	//}
	//return getNodeModifiers(ast)
}

const maxLength = 150
func (ds *documentSymbols) cleanText(text string) string {
	// Truncate to maximum amount of characters as we don't want to do a big replace operation.
	if len(text) > maxLength {
		text = text[:maxLength] + "..."
	}

	// Replaces ECMAScript line terminators and removes the trailing `\` from each line:
	// \n - Line Feed
	// \r - Carriage Return
	// \u2028 - Line separator
	// \u2029 - Paragraph separator
	return regexp.MustCompile(`\\?(\r?\n|\r|\\u2028|\\u2029)`).ReplaceAllLiteralString(text, "")
}

func getNodeSpan(node ast.Node) span.UTF16Span {
	var sourceFile *ast.SourceFile
	var bStart, bEnd int
	if ast.IsSourceFile(node) {
		sourceFile = node.(*ast.SourceFile)
		bStart = getNodeFullStart(node)
		bEnd = getNodeEnd(node)
	} else {
		sourceFile = ast.GetSourceFileOfNode(node)
		bStart = getNodeStart(node, sourceFile, false)
		bEnd = getNodeEnd(node)
	}

	u16span, _ := span.UTF16SpanFromOffsetWithCache(bStart,
		bEnd, sourceFile.Text, sourceFile.GetLineStarts())
	return u16span
}